1 module feature; 2 import commons; 3 public import std.functional:toDelegate; 4 5 ///URLs supports $VERSION 6 struct DownloadURL 7 { 8 string windows; 9 string linux; 10 string osx; 11 12 static DownloadURL any(string url) 13 { 14 return DownloadURL(url, url, url); 15 } 16 17 string get(TargetVersion ver) const 18 { 19 import std.string:replace; 20 string ret; 21 version(Windows) ret = windows; 22 else version(linux) ret = linux; 23 else version(OSX) ret = osx; 24 return ret.replace("$VERSION", ver.toString); 25 } 26 string getDownloadFileName(TargetVersion ver) const 27 { 28 return get(ver).baseName; 29 } 30 } 31 32 struct Download 33 { 34 DownloadURL url; 35 ///Supports $CWD, $TEMP, $VERSION and $NAME 36 string outputPath = "$TEMP$NAME"; 37 ///Negative version ignored. 38 TargetVersion ver; 39 void function(string outputPath) onDownloadFinish; 40 41 bool download(ref Terminal t, ref RealTimeConsoleInput input, TargetVersion ver) 42 { 43 this.ver = ver; 44 commons.downloadWithProgressBar(t, url.get(ver), getOutputPath(ver)); 45 return true; 46 } 47 string getOutputPath() const 48 { 49 return getOutputPath(ver); 50 } 51 52 string getOutputPath(TargetVersion ver) const 53 { 54 import std.conv:to; 55 import std.string; 56 string ret = replace(outputPath, "$CWD", std.file.getcwd); 57 ret = replace(ret, "$TEMP", std.file.tempDir); 58 ret = replace(ret, "$NAME", url.getDownloadFileName(ver)); 59 ret = replace(ret, "$VERSION", ver.toString); 60 return ret; 61 } 62 } 63 64 struct Installation 65 { 66 Download[] downloadsRequired; 67 bool delegate( 68 ref Terminal t, 69 ref RealTimeConsoleInput input, 70 TargetVersion ver, 71 Download[] content 72 ) installer; 73 74 /** 75 * Follows the same order from `downloadsRequired` 76 * May receive null if no extraction is desired for the download. 77 * Accepts $CWD and $VERSION 78 */ 79 string[] extractionPathList; 80 81 string getExtractionPath(size_t index, TargetVersion ver) 82 { 83 import std.string; 84 return extractionPathList[index].replace("$CWD", std.file.getcwd).replace("$VERSION", ver.toString).buildNormalizedPath; 85 } 86 87 bool install(ref Terminal t, ref RealTimeConsoleInput input, TargetVersion ver) 88 { 89 foreach(i, ref d; downloadsRequired) 90 { 91 if(!std.file.exists(d.getOutputPath(ver))) 92 { 93 t.writeln("Downloading ", d.url.get(ver), " --> ", d.getOutputPath(ver)); 94 t.flush; 95 if(!d.download(t, input, ver)) return false; 96 } 97 if(i < extractionPathList.length && extractionPathList[i].length) 98 { 99 import std.file; 100 string extractionPath = getExtractionPath(i, ver); 101 102 if(!extractToFolder(d.getOutputPath(ver), extractionPath, t, input)) 103 return false; 104 } 105 } 106 if(installer) 107 return installer(t, input, ver, downloadsRequired); 108 return true; 109 } 110 111 } 112 113 struct Task(alias Fn) 114 { 115 import std.traits; 116 import std.meta; 117 118 Feature*[] dependencies; 119 private static auto fn = &Fn; 120 static assert(is(Parameters!Fn[0] == Feature*[]), "The first argument of a Task function must be Feature*[]"); 121 122 auto execute(Parameters!Fn[1..$] args) 123 { 124 if(dependencies.length == 0) 125 throw new Error("Your task has no dependency. Maybe you forgot to include it in the mixin StartFeatures! list?"); 126 foreach(dep; dependencies) 127 { 128 if(!dep.getFeature(args[0], args[1])) 129 throw new Exception("Could not get the feature named '"~dep.name~"' for executing the task."); 130 } 131 return fn(dependencies, args); 132 } 133 } 134 135 public import std.system; 136 struct Feature 137 { 138 string name; 139 string description; 140 /** 141 * Checks the existence in $PATH 142 * Checks the existence in gameBuild 143 */ 144 ExistenceChecker existenceChecker; 145 /** 146 * Gets an optional Download[] array, and an installer function 147 * which contains the downloaded files information 148 */ 149 Installation installer; 150 /** 151 * A function that is executed exactly once after the installation 152 * was succeeded. 153 */ 154 void function(ref Terminal t, string where) startUsingFeature; 155 /** 156 * Range of supported versions. May support in the feature also 157 * version whitelisting. 158 */ 159 VersionRange supportedVersion; 160 /** 161 * The version that was actually chosen 162 */ 163 TargetVersion currentVersion; 164 /** 165 * When empty it means it is required on every OS. 166 * This was made because if it is not required in any OS, simply don't 167 * put in the dependencies 168 */ 169 OS[] requiredOn; 170 171 /** 172 * Dependencies must be initialized in a 2-way start. 173 * First, every dependency is started with its own information 174 * After that, all the dependencies are started. 175 */ 176 Feature*[] dependencies; 177 178 bool isRequired() 179 { 180 if(requiredOn.length == 0) 181 return true; 182 foreach(req; requiredOn) if(req == os) return true; 183 return false; 184 } 185 186 187 Feature*[] getAllDependencies() 188 { 189 bool[Feature*] visited; 190 Feature*[] ret; 191 foreach(dep; dependencies) 192 { 193 if(!(dep in visited)) 194 { 195 visited[dep] = true; 196 if(dep.isRequired) 197 { 198 ret~= dep; 199 ret~= dep.getAllDependencies; 200 } 201 } 202 } 203 return ret.unique; 204 } 205 206 private bool startedUsing = false; 207 208 bool getFeature(ref Terminal t, ref RealTimeConsoleInput input, TargetVersion v = TargetVersion.init) 209 { 210 if(v == TargetVersion.init) 211 { 212 if(currentVersion != TargetVersion.init) 213 v = currentVersion; 214 else 215 { 216 v = supportedVersion.max; 217 currentVersion = v; 218 } 219 } 220 if(!supportedVersion.isInRange(v)) 221 { 222 t.writelnError("Unsupported version '",v.toString,"' for feature ", name, 223 ".\n\t Supported versions are ", supportedVersion.toString); 224 return false; 225 } 226 foreach(Feature* dep; getAllDependencies) 227 { 228 if(*dep != Feature.init && !dep.getFeature(t, input, dep.supportedVersion.max)) 229 { 230 t.writelnError("Could not get feature '",name,"': Requires: ", dep.name); 231 return false; 232 } 233 } 234 ExistenceStatus status = existenceChecker.existStatus(t, v); 235 if(status.place == ExistenceStatus.Place.notFound) 236 { 237 import std.conv:to; 238 t.writeln("Installation: ", name, " v", v.toString, "\n\t", description); 239 t.flush; 240 if(!installer.install(t, input, v)) 241 { 242 t.writelnError("Could not install feature ", name); 243 return false; 244 } 245 status = existenceChecker.existStatus(t, v); 246 } 247 if(status.place == ExistenceStatus.Place.notFound) 248 throw new Error(`Could not find `~name~` v`~v.toString~"\n\t"~description~" even after installation"); 249 if(!startedUsing) 250 { 251 startedUsing = true; 252 if(startUsingFeature !is null) 253 { 254 string where = status.where; 255 if(status.place == ExistenceStatus.Place.inConfig) 256 where = configs[where].str; 257 startUsingFeature(t, where); 258 } 259 } 260 return true; 261 } 262 } 263 mixin template StartFeatures(string[] features) 264 { 265 static this() 266 { 267 static foreach(f; features) 268 { 269 mixin("import ",f," = features.",f,";"); 270 mixin(f,".initialize();"); 271 } 272 static foreach(f; features) 273 { 274 mixin(f,".start();"); 275 } 276 } 277 } 278 279 struct ExistenceStatus 280 { 281 static enum Place 282 { 283 notFound, 284 inConfig, 285 inPath, 286 custom 287 } 288 Place place; 289 string where; 290 } 291 292 struct ExistenceChecker 293 { 294 ///All the inputs related to this feature. 295 string[] gameBuildInput; 296 ///All the aliases this feature is expected in path. 297 string[] expectedInPathAs; 298 ///Optional. 299 bool delegate(ref Terminal t, TargetVersion v, out ExistenceStatus where) checkExistenceFn; 300 301 302 ExistenceStatus existStatus(ref Terminal t, TargetVersion v) 303 { 304 ExistenceStatus status; 305 int validCount = 0; 306 foreach(i; gameBuildInput) 307 if(i in configs && std.file.exists(configs[i].str)) validCount++; 308 if(validCount && validCount == gameBuildInput.length) 309 { 310 status.place = ExistenceStatus.Place.inConfig; 311 status.where = gameBuildInput[0]; 312 return status; 313 } 314 foreach(anAlias; expectedInPathAs) 315 { 316 string program = findProgramPath(anAlias); 317 if(program.length) 318 { 319 status.where = program; 320 status. place = ExistenceStatus.Place.inPath; 321 return status; 322 } 323 } 324 if(checkExistenceFn) 325 checkExistenceFn(t, v, status); 326 return status; 327 } 328 329 } 330 struct TargetVersion 331 { 332 static struct Modifier 333 { 334 ///May receive a different modifier name. 335 string name; 336 int ver = -1; 337 } 338 int major = -1; 339 int minor = -1; 340 int patch = -1; 341 Modifier modifier; 342 343 string toString() 344 { 345 import std.conv:to; 346 if(major == -1) return null; 347 string ret = major.to!string; 348 if(minor != -1) ret~= "." ~ minor.to!string; 349 if(patch != -1) ret~= "." ~ patch.to!string; 350 if(modifier.name.length) ret~= modifier.name; 351 if(modifier.ver != -1) ret~= modifier.ver.to!string; 352 return ret; 353 } 354 355 static TargetVersion fromGameBuild(string entry) 356 { 357 if(!(entry in configs)) 358 return TargetVersion.init; 359 return TargetVersion.parse(configs[entry].str); 360 } 361 362 static TargetVersion parse(string ver) 363 { 364 import std.conv:to; 365 string[] vers = ver.split("."); 366 TargetVersion ret; 367 368 if(vers.length > 3) throw new Error("Unsupported format "~ver); 369 if(vers.length > 0) ret.major = vers[0].to!int; 370 if(vers.length > 1) ret.minor = vers[1].to!int; 371 if(vers.length > 2) 372 { 373 import std.algorithm; 374 ptrdiff_t ind = countUntil!((a) => a < '0' || a > '9')(vers[2]); 375 if(ind == -1) ret.patch = vers[2].to!int; 376 else 377 { 378 ret.patch = vers[2][0..ind].to!int; 379 ptrdiff_t ver2 = countUntil!((a) => a >= '0' && a <= '9')(vers[2][ind..$]); 380 if(ver2 == -1) ret.modifier.name = vers[2][ind..$]; 381 else 382 { 383 ret.modifier.name = vers[2][ind..ind+ver2]; 384 ret.modifier.ver = vers[2][ind+ver2..$].to!int; 385 } 386 387 } 388 } 389 return ret; 390 } 391 392 unittest 393 { 394 TargetVersion v = TargetVersion.parse("1.36.0-beta1"); 395 assert(v.toString == "1.36.0-beta1"); 396 assert(v.major == 1); 397 assert(v.minor == 36); 398 assert(v.patch == 0); 399 assert(v.modifier.name == "-beta"); 400 assert(v.modifier.ver == 1); 401 } 402 } 403 404 struct VersionRange 405 { 406 TargetVersion min, max; 407 static VersionRange parse(string min, string max = null) 408 { 409 if(max == null) max = min; 410 return VersionRange(TargetVersion.parse(min), TargetVersion.parse(max)); 411 } 412 413 string toString() 414 { 415 return min.toString ~ " ~ " ~ max.toString; 416 } 417 /** 418 * Compares both major and minor to min and max versions. 419 * Currently, patch, modifier and modifier version are ignored. 420 * Params: 421 * v = A Target version 422 * Returns: 423 */ 424 bool isInRange(TargetVersion v) 425 { 426 if(this == VersionRange.init) 427 return true; 428 return v.major >= min.major && v.major <= max.major && 429 v.minor >= min.minor && v.minor <= max.minor; 430 } 431 }